import queue
import threading
from os import getcwd
from time import sleep
from datetime import datetime
from func_timeout import func_set_timeout, FunctionTimedOut

import socket
from ping3 import ping
from ftplib import FTP
from netifaces import ifaddresses, interfaces
from encodings import idna  # 否则ping()会报错：Unknown encoding idna


def get_local_net_info():
    """通过获取网卡信息，提取到本机所有ipv4地址及对应掩码
    :return: [{}]，每个字典中含有网卡信息
    """
    local_net_info = []
    for interface in interfaces():
        try:
            # ipv4类型的信息是以2为key
            local_net_info.append(ifaddresses(interface)[2][0])
        except:
            pass
    return local_net_info


def st_ping(ip, t=3):
    """使用ping3中的ping方法，t次通一次就返回True
    :param ip: 目的ipv4
    :param t: ping的次数，默认为3
    :return: bool
    """
    while t:
        print('ping {}，{}'.format(ip, t))
        try:
            if ping(ip):
                print('ping 成功')
                return True
            t -= 1
            if t:
                sleep(0.2)
        except Exception as e:
            print(e)
    return False


def port_check(ip, port):
    """使用socket检测目的端口是否打开，但是无法区分关闭状态和过滤状态
    :param ip:目的ipv4
    :param port:目的端口
    :return: bool
    """
    # print('\033[1m*Port %s:%d' % (ip, port)),
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基于internet的、面向连接（TCP）的套接字
    s.settimeout(2)
    try:
        s.connect((ip, port))
        s.shutdown(2)  # 0代表禁止下次的数据读取；1代表禁止下次的数据写入；2代表禁止下次的数据读取和写入。
        print('port ok')
        return True
    except socket.timeout:
        print('port timeout')  # 超时
        return False
    except Exception as e:
        print('m...%s' % e)  # 其它错误
        return False


@func_set_timeout(1)
def ftp_data_connect_check(ftp):
    """这是从ftp_open()独立出来的，在ftp连接上调用一次nlst方法列出远程服务器的当前目录；
    该动作将促使ftp双方尝试建立数据通道，建立不成功此函数则会超时；
    使用func_timeout中提供的装饰器抛出超时异常。

    不设置FTP类里的timeout属性，是为了让它抛出其他更具体的错误，而不都是timeout，
    但这也导致尝试建立数据通道不成功时的等待时间会特别长而无意义，因此独立出来。

    :param ftp: 已经打开的ftp实例
    :return: 无返回
    """
    files = ftp.nlst()
    print(files)


def ftp_open(ip, port, user, passwd, pasv):
    """尝试建立ftp连接，返回过程中的出错信息
    :param ip: 目的ipv4
    :param port: 目的端口
    :param user: 目的ftp服务器登录用户名
    :param passwd: 目的ftp服务器登录密码
    :param pasv: 为True时设置以被动方式发起连接，否则以主动模式发起连接
    :return: 出错显示的字符串及步骤编号组成的元组
    """
    mode = '被动模式' if pasv else '主动模式'
    print('发起{}连接'.format(mode))
    try:
        with FTP(encoding='gb2312') as ftp:
            print(ip, 'user:', user, 'passwd:', passwd, '\n---')
            ftp.set_pasv(pasv)  # 设置ftp主动模式或被动模式
            ftp.set_debuglevel(2)  # 设置调试级别为2以显示更多信息
            try:
                ftp.connect(host=ip, port=port)  # 发起连接
                print(ftp.getwelcome())
                print('FTP 连接成功')
            except Exception as e:
                info = 'FTP 连接失败:{}；'.format(e)
                print('{}'.format(info))
                return info, 5
            try:
                ftp.login(user=user, passwd=passwd)  # 登录
                print('FTP 登录成功')
            except Exception as e:
                info = 'FTP 登录失败:{}；'.format(e)
                print('{}'.format(info))
                return info, 4
            try:
                ftp_data_connect_check(ftp)  # 测试数据连接通道是否畅通，超时则意味着不畅通
                print('FTP {}数据连接成功'.format(mode))
            except FunctionTimedOut:
                ftp.close()  # 如果不直接关闭，将在退出with时使用ftp.quit()，引发ftp 550(文件传送失败)，会特别慢
                info = 'FTP {}数据连接超时；'.format(mode)
                print('{}'.format(info))
                return info, 3
    except Exception as e:
        print(e)
        return e, 2
    return '主动模式成功', 1


res_lock = threading.Lock()


def consumer(name, q, res_path):
    """消费者线程，将不断从FIFO队列中取出服务器信息(ip等)，尝试ping、socket、ftp检测，直到有效信息取完
    :param name: 线程名
    :param q: 共享FIFO队列，包含服务器信息
    :param res_path: 结果储存路径
    :return: 无返回值
    """
    while True:
        connection = q.get()  # 取出
        print(name, connection)
        q.task_done()  # 勿忘，否则会导致程序最后q.join()一直阻塞
        if connection is None:  # 取到生产者放置的生产结束的标志 None，则退出
            q.put(None)  # 退出前留下标志，以示其它消费者线程
            return

        # # 检查子线程资源共享情况用
        # res_lock.acquire()
        # with open(res_path, 'a') as res:
        #     res.write('{} || {} \n'.format(name, connection))
        # res_lock.release()
        # continue

        ip, port, user, passwd, ks = connection
        # 前四个是服务器信息，ks是快速标志，默认快速模式下ping不通则不进行ftp连接
        # ks为0时取消快速模式，ping不通仍然尝试ftp连接
        port = int(port)
        ks = int(ks)

        ping_bool = st_ping(ip) if user != '-' and user != 'anonymous' else st_ping(ip, 1)
        ping_info = 'ping:{}；'.format(ping_bool)

        if ping_bool or not ks:
            port_bool = port_check(ip, port)
            port_info = '远程{}端口:{}；'.format(port, port_bool)
            if port_bool and user != '-':
                info, sig = ftp_open(ip, port, user, passwd, False)  # 最后一个False表示主动模式
                if sig in [2, 3, 5]:
                    # 编号2，3，5，分别代表主动模式其他错误、数据连接超时、控制连接失败；
                    # 这些情况下确认被动模式是否成功，并整理错误信息
                    pasv_info = '被动模式{}；'.\
                        format('成功' if ftp_open(ip, port, user, passwd, True)[1] == 1 else '失败')
                    status = '{}{}{}{}'.format(info, ping_info, port_info, pasv_info)
                else:
                    # 余下的编号4、1，分别代表登录失败、连接畅通，这两种情况下直接记录信息就好
                    status = info
            else:
                status = '{}{}'.format(ping_info, port_info)
        else:
            status = ping_info
        time_info = datetime.now()
        # 写结果
        res_lock.acquire()
        with open(res_path, 'a') as res:
            res.write('{} || {} || {}\n'.format([ip, port, user, passwd], status, time_info))
        res_lock.release()
        print('---')


def scan_net(q, connection):
    """生产者的工具函数，负责生产局域网内ip对应的connection
    :param q: 共享FIFO队列，放置服务器信息
    :param connection: 包含port, user, passwd, ks部分的服务器信息
    :return: 无返回值
    """
    net_info = get_local_net_info()
    for info in net_info:
        print(info)
        local_ip = info['addr']
        if local_ip == '127.0.0.1' or local_ip == '192.168.56.1':
            continue
        list_mask = info['netmask'].split('.')
        ip_mask_zip = zip(local_ip.split('.'), list_mask)
        list_net = list(map(lambda x: int(x[0]) & int(x[1]), ip_mask_zip))  # ip与mask按位与得到网络号
        list_max = list(map(lambda x: int(x) ^ 0xff, list_mask))  # 掩码按位取反得子网每字节能加的数；异或避免符号位也取反

        # 将子网内的ip都遍历出来
        for h in range(list_max[0] + 1):
            for i in range(list_max[1] + 1):
                for j in range(list_max[2] + 1):
                    for k in range(list_max[3] + 1):
                        if h + i + j + k == 0:  # 网络号不要ping
                            continue
                        local_net_ip = '{}.{}.{}.{}'.format(list_net[0] + h, list_net[1] + i,
                                                            list_net[2] + j, list_net[3] + k)
                        if local_net_ip in [local_ip, info['broadcast']]:
                            continue
                        scan_collection = [local_net_ip] + connection
                        # print(scan_collection)
                        q.put(scan_collection)


def producer(name, q, config_path):
    """生产者线程，提取配置文件中的服务器信息并放入队列
    :param name: 线程名
    :param q: 共享FIFO队列，放置服务器信息
    :param config_path: 配置信息路径
    :return: 无返回值
    """
    with open(config_path, 'r', encoding='utf-8') as file:
        content = file.readline()
        while content:
            connection = content.strip().split(' ')
            if len(connection) in [4, 5] and connection[0][0] != '#':  # 解析服务器信息，跳过注释

                if len(connection) == 4:  # 说明末尾不带*号，说明选择快速模式，consumer会用到
                    connection.append('1')
                else:
                    connection[4] = '0'

                print(connection)

                # 切换到自动扫描获取ip模式
                if connection[0] == 'local':
                    scan_net(q, connection[1:5])
                    content = file.readline()
                    continue

                # 对含范围的ip及port，循环遍历出范围内的所有组合
                dst_ip_port_combine = '.'.join(connection[0:2])
                if '-' in dst_ip_port_combine:
                    all_bytes = map(lambda x: x.split('-'), dst_ip_port_combine.split('.'))
                    # 将ip及port处理成二维数组，里层是因为一个位置的数字可能是范围表示

                    start = []
                    end = []
                    for i, byte in enumerate(all_bytes):  # 先找到每个位置范围的上下限
                        start.append(int(byte[0]))
                        end.append(start[i])
                        if len(byte) > 1:
                            end[i] = int(byte[1])

                    # 枚举用户写下的范围中所有ip、port组合
                    # 假如用户写了一段范围，那么下面循环的复杂度就是On，两段范围就是On^2
                    for h in range(start[0], end[0] + 1):
                        for i in range(start[1], end[1] + 1):
                            for j in range(start[2], end[2] + 1):
                                for k in range(start[3], end[3] + 1):
                                    for p in range(start[4], end[4] + 1):
                                        every_connection = ['{}.{}.{}.{}'.format(h, i, j, k),
                                                            '{}'.format(p)] + connection[2:5]
                                        # print(name, every_connection)
                                        q.put(every_connection)
                else:  # 精确地址不用其它处理
                    # print(name, connection)
                    q.put(connection)
            content = file.readline()
        q.put(None)


def main():
    config_path = getcwd() + '\\ftpCheckConfig.txt'
    res_path = getcwd() + '\\ftpCheckRes.txt'

    q = queue.Queue(maxsize=300)

    threads = []
    p = threading.Thread(target=producer, args=('A', q, config_path))  # 创建生产者子线程提取服务器信息
    threads.append(p)
    for i in range(200):
        c = threading.Thread(target=consumer, args=(i, q, res_path))  # 创建消费者子线程根据服务器信息检测连接
        threads.append(c)

    for i in threads:
        i.daemon = True  # 设置为守护线程
        i.start()  # 启动线程
    for i in threads:
        i.join()  # 阻塞以等待子线程结束

    while not q.empty():  # 清空队列
        q.get()
        q.task_done()
    q.join()  # 等待队列空


main()
